---
title: 使用 DQL 检索数据
---
SELECT 是 SQL 中最常见的关键字之一，除了 SELECT 之外，还有 INSERT、DELETE、UPDATE 等关键字，这些关键字是 SQL 的保留字。

## 查询的基础语法

数据表是由列（字段名）和行（数据行）组成的，我们要返回满足条件的数据行，就需要在 FROM 后面加上我们要查询的表名， SELECT 后面加上我们想要查询的列名，可以是一列，也可以是多个列。

我创建了一个王者荣耀英雄数据表，这张表里一共有 69 个英雄，23 个属性值（不包括英雄名 name）。[点击下载这里 SQL 文件](https://wkq-img.oss-cn-chengdu.aliyuncs.com/heros_data_with_structure.sql)

表中 24 个字段，分别代表的含义如下。

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241126233459.png"/>

### 查询列
想要对数据表中的某一列进行检索，在 SELECT 后面加上这个列的字段名即可。比如我们想要检索数据表中都有哪些英雄。
```sql
SELECT name FROM heros
```
也可以对多个列进行检索，在列名之间用逗号 (,) 分割即可。比如我们想要检索有哪些英雄，他们的最大生命、最大法力、最大物攻和最大物防分别是多少。
```sql
SELECT name, hp_max, mp_max, attack_max, defense_max FROM heros
```
表中一共有 25 个字段，除了 id 和英雄名 name 以外，还存在 23 个属性值, 可以使用 * 代表所有列：

```sql
SELECT * FROM heros
```
### 起别名
在使用 SELECT 查询的时候，可以给列名起别名。比如给英雄名、最大生命、最大法力、最大物攻和最大物防等取别名：

```sql
SELECT name AS n, hp_max AS hm, mp_max AS mm, attack_max AS am, defense_max AS dm FROM heros
```

运行结果和上面多列检索的运行结果是一样的，只是将列名改成了 n、hm、mm、am 和 dm。当然这里的列别名只是举例，一般来说起别名的作用是对原有名称进行简化，从而让 SQL 语句看起来更精简。同样我们也可以对表名称起别名，这个在多表连接查询的时候会用到。

### 去除重复行
可以使用关键字 DISTINCT 去除查询列中的重复行。
```sql
SELECT DISTINCT attack_range FROM heros
```
如果我们带上英雄名称:

```sql
SELECT DISTINCT attack_range, name FROM heros
```

1. DISTINCT 需要放到所有列名的前面.
2. DISTINCT 其实是对后面所有列名的组合进行去重。


### 排序检索数据
检索数据时，有时需要按照某种顺序进行结果的返回，就需要用到 ORDER BY 子句。

```sql title='显示英雄名称及最大生命值，按照最大生命值从高到低的方式进行排序'
SELECT name, hp_max FROM heros ORDER BY hp_max DESC
```
```sql title='显示英雄名称及最大生命值，按照第一排序最大法力从低到高，当最大法力值相等的时候则按照第二排序进行，即最大生命值从高到低的方式进行排序'
SELECT name, hp_max FROM heros ORDER BY mp_max, hp_max DESC
```

使用 ORDER BY 子句需要掌握以下几点：
1. 排序的列名：ORDER BY 后面可以有一个或多个列名，如果是多个列名进行排序，会按照后面第一个列先进行排序，当第一列的值相同的时候，再按照第二列进行排序，以此类推。
2. 排序的顺序：ORDER BY 后面可以注明排序规则，ASC 代表递增排序，DESC 代表递减排序。如果没有注明排序规则，默认情况下是按照 ASC 递增排序。
3. 非选择列排序： ORDER BY 可以使用非选择列进行排序，所以即使在 SELECT 后面没有这个列名，你同样可以放到 ORDER BY 后面进行排序。
4. ORDER BY 的位置：ORDER BY 通常位于 SELECT 语句的最后一条子句，否则会报错。

### 约束返回结果的数量
在查询过程中，使用 LIMIT 关键字，可以约束返回结果的数量。

```sql title='返回英雄名称及最大生命值，按照最大生命值从高到低排序，返回 5 条记录即可'
SELECT name, hp_max FROM heros ORDER BY hp_max DESC LIMIT 5
```

约束返回结果的数量可以减少数据表的网络传输量，也可以提升查询效率。

### SELECT 的执行顺序
查询是 RDBMS 中最频繁的操作。需要记住 SELECT 查询时的两个顺序：

#### 关键字的顺序是不能颠倒的
```sql
// ... 表示省略号
SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY ...
```
HAVING 依赖 GROUP BY, 不可单独出现。
#### 语句的执行顺序

```sql
FROM > WHERE > GROUP BY > HAVING > SELECT的字段 > DISTINCT > ORDER BY > LIMIT
```

比如下面这个sql ，它的关键字顺序和执行顺序是下面这样的：

```sql
SELECT DISTINCT player_id, player_name, count(*) as num #顺序5
FROM player JOIN team ON player.team_id = team.team_id #顺序1
WHERE height > 1.80 #顺序2
GROUP BY player.team_id #顺序3
HAVING num > 2 #顺序4
ORDER BY num DESC #顺序6
LIMIT 2 #顺序7
```
在 SELECT 语句执行这些步骤的时候，每个步骤都会产生一个虚拟表，然后将这个虚拟表传入下一个步骤中作为输入。这些步骤隐含在在 SQL 的执行过程中，对于我们来说是不可见的。

ELECT 是先执行 FROM 这一步的。在这个阶段，如果是多张表联查，还会经历下面的几个步骤：
1. 首先先通过 CROSS JOIN 求笛卡尔积，相当于得到虚拟表 vt（virtual table）1-1；
2. 通过 ON 进行筛选，在虚拟表 vt1-1 的基础上进行筛选，得到虚拟表 vt1-2；
3. 添加外部行。如果我们使用的是左连接、右链接或者全连接，就会涉及到外部行，也就是在虚拟表 vt1-2 的基础上增加外部行，得到虚拟表 vt1-3。

当然如果我们操作的是两张以上的表，还会重复上面的步骤，直到所有表都被处理完为止。这个过程得到是我们的原始数据。

查询到数据表的原始数据，也就是最终的虚拟表 vt1，就可以在此基础上再进行 WHERE 阶段。在这个阶段中，会根据 vt1 表的结果进行筛选过滤，得到虚拟表 vt2。

然后进入第三步和第四步，也就是 GROUP 和 HAVING 阶段。在这个阶段中，实际上是在虚拟表 vt2 的基础上进行分组和分组过滤，得到中间的虚拟表 vt3 和 vt4。

完成了条件筛选部分之后，就可以筛选表中提取的字段，也就是进入到 SELECT 和 DISTINCT 阶段。

首先在 SELECT 阶段会提取想要的字段，然后在 DISTINCT 阶段过滤掉重复的行，分别得到中间的虚拟表 vt5-1 和 vt5-2。

提取了想要的字段数据之后，就可以按照指定的字段进行排序，也就是 ORDER BY 阶段，得到虚拟表 vt6。

最后在 vt6 的基础上，取出指定行的记录，也就是 LIMIT 阶段，得到最终的结果，对应的是虚拟表 vt7。

在写查询语句的时候，不一定需要所有的关键字，不需要的阶段会被省略。
## 数据过滤

提升查询效率的一个很重要的方式，就是约束返回结果的数量，还有一个很有效的方式，就是指定筛选条件，进行过滤。减少不必要的数据行。这里主要使用的就是 WHERE 子句。


### 比较运算符
使用 WHERE 子句对条件进行筛选，需要用到下面的比较运算符。


<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241127004435.png"/>

可以是（`<=`），也可以是不大于（`!>`）。同样不等于，可以用（`<>`），也可以用（`!=`），它们的含义都是相同的，但这些符号的顺序都不能颠倒，比如你不能写（`=<`）。

WHERE 子句的基本格式是：<RedSpan>SELECT ……(列名) FROM ……(表名) WHERE ……(子句条件)</RedSpan>

```sql title='查询所有最大生命值大于 6000 的英雄'
SELECT name, hp_max FROM heros WHERE hp_max > 6000
```


```sql title='查询所有最大生命值在 5399 到 6811 之间的英雄'
SELECT name, hp_max FROM heros WHERE hp_max BETWEEN 5399 AND 6811
```

BETWEEN 包含最大值和最小值。

```sql title='hp_max字段为空的数据'
SELECT name, hp_max FROM heros WHERE hp_max IS NULL
```
### 逻辑运算

如果存在多个 WHERE 条件子句，可以使用逻辑运算符：

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241127004923.png"/>

```sql title='筛选最大生命值大于 6000，最大法力大于 1700 的英雄，然后按照最大生命值和最大法力值之和从高到低进行排序'
SELECT name, hp_max, mp_max FROM heros WHERE hp_max > 6000 AND mp_max > 1700 ORDER BY (hp_max+mp_max) DESC
```

```sql title= '查询最大生命值加最大法力值大于 8000 的英雄，或者最大生命值大于 6000 并且最大法力值大于 1700 的英雄。'
SELECT name, hp_max, mp_max FROM heros WHERE (hp_max+mp_max) > 8000 OR hp_max > 6000 AND mp_max > 1700 ORDER BY (hp_max+mp_max) DESC
```
当 WHERE 子句中同时存在 OR 和 AND 的时候，AND 执行的优先级会更高，也就是说 SQL 会优先处理 AND 操作符，然后再处理 OR 操作符。


```sql title='对上面这条查询语句 OR 两边的条件增加一个括号'
SELECT name, hp_max, mp_max FROM heros WHERE ((hp_max+mp_max) > 8000 OR hp_max > 6000) AND mp_max > 1700 ORDER BY (hp_max+mp_max) DESC
```
当 WHERE 子句中同时出现 AND 和 OR 操作符的时候，需要考虑到执行的先后顺序，() 优先级最高，其次优先级是 AND，然后是 OR。

```sql title='查询主要定位或者次要定位是法师或是射手的英雄，同时英雄的上线时间不在 2016-01-01 到 2017-01-01 之间'
SELECT name, role_main, role_assist, hp_max, mp_max, birthdate
FROM heros
WHERE (role_main IN ('法师', '射手') OR role_assist IN ('法师', '射手'))
AND DATE(birthdate) NOT BETWEEN '2016-01-01' AND '2017-01-01'
ORDER BY (hp_max + mp_max) DESC
```
### 使用通配符进行过滤
检索文本中包含某个词的所有数据，这里就需要使用通配符。通配符就是我们用来匹配值的一部分的特殊字符。这里需要使用到 LIKE 操作符。

* % : 匹配零个或多个字符
* _ : 匹配单个字符

```sql title='查找英雄名中包含“太”字的英雄都有哪些'
SELECT name FROM heros WHERE name LIKE '%太%'
```
```sql title = '查找英雄名除了第一个字以外，包含‘太’字的英雄有哪些'
SELECT name FROM heros WHERE name LIKE '_%太%'
```